iT邦幫忙

2023 iThome 鐵人賽

DAY 15
0
DevOps

大家都在用 Terraform 實作 IaC 為什麼不將程式寫得更簡潔易讀呢?系列 第 15

實作 AWS 常用服務之 Terraform 模組系列 - IAM 篇

  • 分享至 

  • xImage
  •  

AWS IAM 模組實作

本篇是實作常用的 AWS IAM 服務之 Terraform 模組,並且會使用到 YAML 資料結構來定義模組的內容,完整的專案程式碼分享在我的 Github 上。

  1. 先定義整個專案檔案結構設定檔 ./configs/iam/iam.yaml 與五個放置 policy JSON 檔案位置的目錄:
    • ./configs/iam/assume_role_policies: 放置 assume role policy 的目錄
    • ./configs/iam/group_policies: 放置 inline group policy 的目錄
    • ./configs/iam/policies: 放置 policy 的目錄
    • ./configs/iam/role_policies: 放置 inline role policy 的目錄
    • ./configs/iam/user_policies: 放置 inline user policy 的目錄
  2. 模組 my_iam 的放置位置 modules/my_iam:
├── configs
│   ├── iam
│   │   ├── assume_role_policies
│   │   ├── group_policies
│   │   ├── policies
│   │   ├── role_policies
│   │   ├── user_policies
│   │   └── iam.yaml
│   ├── subnet
│   └── vpc
├── example.tfvars
├── locals.tf
├── main.tf
├── modules
│   ├── my_iam
│   │   ├── iam_group.tf
│   │   ├── iam_group_policy.tf
│   │   ├── iam_group_policy_attachment.tf
│   │   ├── iam_instance_profile.tf
│   │   ├── iam_policy.tf
│   │   ├── iam_role.tf
│   │   ├── iam_role_policy.tf
│   │   ├── iam_role_policy_attachment.tf
│   │   ├── iam_user.tf
│   │   ├── iam_user_group_membership.tf
│   │   ├── iam_user_policy.tf
│   │   ├── iam_user_policy_attachment.tf
│   │   ├── output.tf
│   │   ├── provider.tf
│   │   └── variables.tf
│   ├── my_igw
│   ├── my_instances
│   ├── my_nacls
│   ├── my_route_tables
│   ├── my_subnets
│   └── my_vpc
└── variables.tf
  1. 撰寫 ./configs/iam/iam.yaml 內容來定義 IAM 需要用建立的資源:
# Define Policy
policies: []
# Emample: 
# - name: "The name of policy"
#   description: "The description of policy"
#   json_file: ./configs/iam/policies/???????.json
#   path: "/"

# Define user, bind user group and attch existing policy
users: []
# Example:
# - name: "The name of user"
#   groups:
#     - "The name of group"
#   inline_policies:
#     - name: "The name of inline policy"
#     - json_file: ./configs/iam/user_policies/???????.json
#   policy_arns: []
#     - arn:aws:iam::????????????:policy/???????

# Define user group, attch inline and existing policies
groups: []
# Example
# - name: "The name of group"
#   inline_policies:
#     - name: "The name of inline policy"
#     - json_file: ./configs/iam/group_policies/???????.json
#   policy_arns:
#     - arn:aws:iam::????????????:policy/???????

# Define role, attch inline and existing policies
roles: []
# Example
# - name: "The name of role"
#   description: "The description of role"
#   assume_role_policy_json_file: ./configs/iam/assume_role_policies/???????.json
#   inline_policies:
#     - name: "The name of inline policy"
#     - json_file: ./configs/iam/role_policies/???????.json
#   policy_arns: []
#     - arn:aws:iam::????????????:policy/???????
#   tag_name: ""
#   path: "/"

# Define instance profiles
instance_profiles: []
# Example
# - name: "The name of instance profile"
#   role: "The role name of instance profile"

  1. 撰寫 my_iam 模組
  • ./modules/my_iam/outputs.tf:
output "iam_group_arn" {
  value = aws_iam_group.groups
}

output "iam_role_arn" {
  value = aws_iam_role.roles
}

output "iam_user_arn" {
  value = aws_iam_user.users
}

  • ./modules/my_iam/provider.tf:
provider "aws" {
    region  = var.aws_region
    profile = var.aws_profile
}
  • ./modules/my_iam/variables.tf:
variable "aws_region" {
  description = "AWS region"
  default     = "ap-northeast-1"
}

variable "aws_profile" {
  description = "AWS profile"
  default     = ""
}

variable "project_name" {
  type    = string
  description = "Project name"
  default = ""
}

variable "department_name" {
  type        = string
  description = "Department name"
  default     = "SRE"
}

variable "iam_path" {
  type    = string
  default = ""
}

  • ./modules/my_iam/iam_group.tf:
    變數 iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 groups 的 value 為一 list 物件
locals {
  groups = yamldecode(file("${var.iam_path}"))["groups"]
}

resource "aws_iam_group" "groups" {

  for_each = { for r in local.groups : r.name => r }

  name = each.value.name
  path = "/"
}

  • ./modules/my_iam/iam_group_policy.tf:
    利用 flattern 函式將 group.nameinline_policies 轉換成單一列表
locals {
  group_policies = flatten([
    for group in local.groups : [
      for inline_policy in lookup(group, "inline_policies", []) : {
        name      = inline_policy.name
        json_file = inline_policy.json_file
        group     = group.name
      }
    ]
  ])
}

resource "aws_iam_group_policy" "group_policies" {

  for_each = { for r in local.group_policies : "${r.group}:${r.name}" => r }

  name = each.value.name

  policy = file("${each.value.json_file}")

  group = each.value.group

  depends_on = [
    aws_iam_group.groups
  ]
}

  • ./modules/my_iam/iam_group_policy_attachment.tf:
    利用 flattern 函式將 group.namepolicy_arns 轉換成單一列表
locals {
  group_policy_association_list = flatten([
    for group in local.groups : [
      for policy_arn in lookup(group, "policy_arns", []) : {
        group      = group.name
        policy_arn = policy_arn
      }
    ]
  ])
}

resource "aws_iam_group_policy_attachment" "attachments" {

  for_each = { for r in local.group_policy_association_list : "${r.group}/${r.policy_arn}" => r }

  policy_arn = each.value.policy_arn
  group      = each.value.group

  depends_on = [
    aws_iam_policy.policies,
    aws_iam_group.groups
  ]
}

  • ./modules/my_iam/iam_instance_profile.tf:
    變數 iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 instance_profiles 的 value 為一 list 物件
locals {
  instance_profiles = yamldecode(file("${var.iam_path}"))["instance_profiles"]
}

resource "aws_iam_instance_profile" "instance_profile" {

  for_each = { for r in local.instance_profiles : r.name => r }

  name = each.value.name
  role = each.value.role

  tags = {
    Department = var.department_name
    Name       = each.value.name
    Project    = var.project_name
  }

  tags_all = {
    Department = var.department_name
    Name       = each.value.name
    Project    = var.project_name
  }
}

  • ./modules/my_iam/iam_policy.tf:
    變數 iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 policies 的 value 為一 list 物件
locals {
  policies = yamldecode(file("${var.iam_path}"))["policies"]
}

resource "aws_iam_policy" "policies" {

  for_each = { for r in local.policies : r.name => r }

  description = each.value.description
  name        = each.value.name
  path        = each.value.path

  policy = file("${each.value.json_file}")

  tags = {
    Department = var.department_name
    Project    = var.project_name
  }

  tags_all = {
    Department = var.department_name
    Project    = var.project_name
  }
}

  • ./modules/my_iam/iam_role.tf:
    變數 iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 roles 的 value 為一 list 物件
locals {
  roles = yamldecode(file("${var.iam_path}"))["roles"]
}

resource "aws_iam_role" "roles" {

  for_each = { for r in local.roles : r.name => r }

  assume_role_policy = each.value.assume_role_policy_json_file != "" ? file("${each.value.assume_role_policy_json_file}") : <<POLICY
  {}
  POLICY

  description          = each.value.description
  max_session_duration = "3600"
  name                 = each.value.name
  path                 = each.value.path

  tags = {
    Department = var.department_name
    Name       = each.value.tag_name
    Project    = var.project_name
  }

  tags_all = {
    Department = var.department_name
    Name       = each.value.tag_name
    Project    = var.project_name
  }
}

  • ./modules/my_iam/iam_role_policy.tf:
    利用 flattern 函式將 role.nameinline_policies 轉換成單一列表
locals {
  role_policies = flatten([
    for role in local.roles : [
      for inline_policy in lookup(role, "inline_policies", []) : {
        name      = inline_policy.name
        json_file = inline_policy.json_file
        role      = role.name
      }
    ]
  ])
}

resource "aws_iam_role_policy" "role_policies" {

  for_each = { for r in local.role_policies : "${r.role}:${r.name}" => r }

  name = each.value.name

  policy = file("${each.value.json_file}")

  role = each.value.role

  depends_on = [
    aws_iam_role.roles
  ]
}

  • ./modules/my_iam/iam_role_policy_attachment.tf:
    利用 flattern 函式將 role.namepolicy_arns 轉換成單一列表
locals {
  role_policy_association_list = flatten([
    for role in local.roles : [
      for policy_arn in lookup(role, "policy_arns", []) : {
        role       = role.name
        policy_arn = policy_arn
      }
    ]
  ])
}

resource "aws_iam_role_policy_attachment" "attachments" {

  for_each = { for r in local.role_policy_association_list : "${r.role}/${r.policy_arn}" => r }

  policy_arn = each.value.policy_arn
  role       = each.value.role

  depends_on = [
    aws_iam_policy.policies,
    aws_iam_role.roles
  ]
}

  • ./modules/my_iam/iam_user.tf:
    變數 iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 users 的 value 為一 list 物件
locals {
  users = yamldecode(file("${var.iam_path}"))["users"]
}

resource "aws_iam_user" "users" {
  # checkov:skip=  CKV_AWS_273: need to do it later

  for_each = { for r in local.users : r.name => r }

  name = each.value.name
  path = "/"

  tags = {
    Department = var.department_name
    Project    = var.project_name
  }

  tags_all = {
    Department = var.department_name
    Project    = var.project_name
  }
}

  • ./modules/my_iam/iam_user_group_membership.tf:
    變數 iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 users 的 value 為一 list 物件
locals {
  user_groups = yamldecode(file("${var.iam_path}"))["users"]
}

resource "aws_iam_user_group_membership" "user_groups" {

  for_each = { for r in local.user_groups : r.name => r if length(r.groups) > 0 }

  groups = each.value.groups
  user   = each.value.name

  depends_on = [
    aws_iam_user.users
  ]
}

  • ./modules/my_iam/iam_user_policy.tf:
    利用 flattern 函式將 user.nameinline_polices 轉換成單一列表
locals {
  user_policies = flatten([
    for user in local.users : [
      for inline_policy in lookup(user, "inline_policies", []) : {
        name      = inline_policy.name
        json_file = inline_policy.json_file
        user      = user.name
      }
    ]
  ])
}

resource "aws_iam_user_policy" "user_policies" {

  for_each = { for r in local.user_policies : "${r.user}:${r.name}" => r }

  name = each.value.name

  policy = file("${each.value.json_file}")

  user = each.value.user

  depends_on = [
    aws_iam_user.users
  ]
}

  • ./modules/my_iam/iam_user_policy_attachment.tf:
    利用 flattern 函式將 user.namepolicy_arns 轉換成單一列表
locals {
  user_policy_association_list = flatten([
    for user in local.users : [
      for policy_arn in lookup(user, "policy_arns", []) : {
        user       = user.name
        policy_arn = policy_arn
      }
    ]
  ])
}

resource "aws_iam_user_policy_attachment" "attachments" {

  for_each = { for r in local.user_policy_association_list : "${r.user}/${r.policy_arn}" => r }

  policy_arn = each.value.policy_arn
  user       = each.value.user

  depends_on = [
    aws_iam_policy.policies,
    aws_iam_user.users
  ]
}

  1. 撰寫專案相關程式
  • example.tfvars:
aws_region="ap-northeast-1"
aws_profile="<YOUR_PROFILE>"
project_name="example"
department_name="SRE"
  • main.tf:
terraform {
  required_providers {
    aws = {
      version = "5.15.0"
    }
  }

  backend "s3" {
    bucket                  = "<YOUR_S3_BUCKET_NAME>"
    dynamodb_table          = "<YOUR_DYNAMODB_TABLE_NAME>"
    key                     = "terraform.tfstate"
    region                  = "ap-northeast-1"
    shared_credentials_file = "~/.aws/config"
    profile                 = "<YOUR_PROFILE>"
  }
}

# 其他模組省略

# iam
module "iam" {
  aws_profile     = var.aws_profile
  aws_region      = var.aws_region
  department_name = var.department_name
  project_name    = var.project_name
  iam_path        = "./configs/iam/iam.yaml"

  source = "./modules/my_iam"
}


Terraform 執行計畫

  1. 先建立一個 IAM User 與 Inline User Policy 測試一下模組:
# Define Policy
policies: []

# Define user, bind user group and attch existing policy
users:
  - name: admin-user
    groups: []
    inline_policies:
      - name: admin_access
        json_file: "./configs/iam/user_policies/admin_access.json"
    policy_arns: []

# Define user group, attch inline and existing policies
groups: []

# Define role, attch inline and existing policies
roles: []

instance_profiles: []

  1. 於專案目錄下執行 terraform init && terraform plan --out .plan -var-file=example.tfvars 來確認一下結果:
  # module.iam.aws_iam_user.users["admin-user"] will be created
  + resource "aws_iam_user" "users" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "admin-user"
      + path          = "/"
      + tags          = {
          + "Department" = "SRE"
          + "Project"    = "example"
        }
      + tags_all      = {
          + "Department" = "SRE"
          + "Project"    = "example"
        }
      + unique_id     = (known after apply)
    }

  # module.iam.aws_iam_user_policy.user_policies["admin-user:admin_access"] will be created
  + resource "aws_iam_user_policy" "user_policies" {
      + id     = (known after apply)
      + name   = "admin_access"
      + policy = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = "*"
                      + Effect   = "Allow"
                      + Resource = "*"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + user   = "admin-user"
    }

下一篇文章將會展示實作 AWS S3 之 Terraform 模組。


上一篇
實作 AWS 常用服務之 Terraform 模組系列 - Route Table 篇
下一篇
實作 AWS 常用服務之 Terraform 模組系列 - S3 篇
系列文
大家都在用 Terraform 實作 IaC 為什麼不將程式寫得更簡潔易讀呢?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言